/**
 * DSM - Distributed Search Manager
 * Author: gsingh@educationau.edu.au
 * $Revision: 1.10 $
 * $Date: 2007/08/10 06:49:42 $
 *
 * (C)opyright 2004 education.au pty limited (ABN 88 058 020 876) of
 * 178 Fullarton Road, Dulwich, South Australia 5065, Australia.
 *
 * This software was developed using funding and other support provided
 * by the Commonwealth of Australia's Department of Education, Science
 * and Training.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of Version 2 of the GNU General Public License as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * See Version 2 of the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the
 *
 *    Free Software Foundation, Inc.,
 *    59 Temple Place, Suite 300,
 *    Boston, MA 02111-1307 USA.
 *
 * You can contact education.au pty limited by mail at
 *
 *    178 Fullarton Road,
 *    Dulwich, South Australia 5065,
 *    Australia
 *
 *    or
 *
 *    email opensource@educationau.edu.au.
 *
 */

package au.edu.educationau.opensource.dsm.adapters;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import au.edu.educationau.opensource.dsm.DeusExMachina;
import au.edu.educationau.opensource.dsm.obj.SMEvents;
import au.edu.educationau.opensource.dsm.obj.SearchCriteria;
import au.edu.educationau.opensource.dsm.obj.SearchResult;
import au.edu.educationau.opensource.dsm.util.Flog;
import au.edu.educationau.opensource.dsm.worker.ResultSetUnifier;

/**
 * This is the base class for all the Search Adapters. It has a state machine in
 * it that allows auto processing of each step to obtain results.
 */

public abstract class SearchAdapter {

	/** URL Buffer allows the request URL to be assembled on the fly */
	private StringBuffer urlBuffer = new StringBuffer();

	/** The properties for this Search Adapter */
	private SearchAdapterProperties properties = null;

	/** The properties for this Search Adapter */
	private SearchCriteria criteria = null;

	/** The unique adapter batch code for identification within the job */
	private String adapterInstanceTag = "";

	/** The total number of results found */
	private int totalFound = 0;

	/**
	 * Theoretical Total Number of results available - could be much more than
	 * required
	 */
	private int theoreticalAvailableResults = 0;

	/** The status of this adapter. It is updated by the statemachine */
	private short status = SMEvents.SEARCH_ADAPTER_STATUS_IDLE;

	/** The diagnostic code as set by an adapter if it has it */
	private String diagnosticCode = "";

	/** The response code as set by the adapter */
	private short responseCode = 0;

	/** The adapter has finished its processing - fireStateMachine */
	private boolean finished = false;

	/**
	 * A flag that is checked at each step of the statemachine to ensure that
	 * the job is still active superEgo gets set to true (activated) to stop the
	 * processing
	 */
	private boolean superEgo = false;

	/**
	 * A map of facet queries (Lucene format) to result set counts.
	 */
	private Map facetQueryCounts = new HashMap();

	/**
	 * Constructor
	 */
	public SearchAdapter() {
	}

	/**
	 * Sets the properties for this adapter.
	 * 
	 * @param properties
	 */
	public void setProperties(SearchAdapterProperties properties) {
		this.properties = properties;
	}

	/**
	 * Returns this adapter's properties
	 * 
	 * @return SearchAdapterProperties object
	 */
	public SearchAdapterProperties getProperties() {
		return this.properties;
	}

	/**
	 * Updates the search result cache entry with the time taken to process this
	 * adapter.
	 * 
	 * @param time
	 */
	public void calculateTimeTaken(long time) {
		DeusExMachina._SearchResultService().calculateTimeTaken(getSearchCriteria().getToken(), time, this.properties.getAdapterCode());
	}

	/**
	 * Enumerates this adapter's code to make a unique code within a job
	 * 
	 * @param enumerator
	 */
	public void enumerateAdapterInstanceTag(int enumerator) {
		this.adapterInstanceTag = properties.getAdapterCode() + enumerator;
	}

	/**
	 * Returns the enumerated Adapter instance tag
	 * 
	 * @return adapterInstanceTag
	 */
	public String getAdapterInstanceTag() {
		return this.adapterInstanceTag;
	}

	/**
	 * Set the status of this adapter.
	 * 
	 * @param status
	 */
	protected synchronized void setStatus(short status) {
		this.status = status;

		if (this.responseCode <= 0) {
			setResponseCode((int) status);
		}
	}

	/**
	 * Returns the status of this adapter
	 * 
	 * @return status
	 */
	public short getStatus() {
		return status;
	}

	/**
	 * Set the response Code of this adapter. If the adapter has completed and
	 * the responseCode has not set then it returns the adapter's status.
	 * 
	 * @param status
	 */
	protected void setResponseCode(int responseCode) {
		this.responseCode = (short) responseCode;

		DeusExMachina._SearchResultService().setResponseCode(getSearchCriteria().getToken(), this.properties.getAdapterCode(), "" + responseCode);
	}

	/**
	 * Returns the response code of this adapter
	 * 
	 * @return adapter responsecode
	 * 
	 */
	public short getResponseCode() {
		return responseCode;
	}

	/**
	 * Append this String block to the end of the request URL Buffer.
	 * 
	 * @param block
	 */
	public void addToURLBuffer(String block) {
		urlBuffer.append(block);
	}

	/**
	 * Add this integer block to the end of the request URL Buffer
	 * 
	 * @param block
	 */
	public void addToURLBuffer(int block) {
		urlBuffer.append(block);
	}

	/**
	 * Add this float block to the end of the request URL Buffer
	 * 
	 * @param block
	 */
	public void addToURLBuffer(float block) {
		urlBuffer.append(block);
	}

	/**
	 * Add this boolean block to the end of the request URL Buffer
	 * 
	 * @param block
	 */
	public void addToURLBuffer(boolean block) {
		urlBuffer.append(block);
	}

	/**
	 * Clear the request URL Buffer
	 * 
	 */
	public void clearURLBuffer() {
		urlBuffer = new StringBuffer();
	}

	/**
	 * Return the URL Buffer
	 * 
	 * @return urlBuffer
	 */
	public StringBuffer getURLBuffer() {
		return urlBuffer;
	}

	/**
	 * Modifies the Request URL depending on the request type. For GET requests,
	 * add a ? to the end if there are no default parameters, otherwise add an
	 * &amp; For POST requests, return the URL as is.
	 * 
	 * @return url the search URL modified to suit the request METHOD
	 */
	public String getAugmentedSearchURL() {
		if (properties.getMethod().equals("GET")) {
			StringBuffer url = new StringBuffer();
			url.append(properties.getSearchURL());
			if (url.indexOf("?") < 0) {
				url.append("?");
			} else {
				url.append("&");
			}
			url.append(urlBuffer.toString());
			return url.toString();
		} else {
			return properties.getSearchURL();
		}
	}

	/**
	 * Sets the search criteria for this adapter
	 * 
	 * @param criteria
	 */
	public void setSearchCriteria(SearchCriteria criteria) {
		this.criteria = criteria;
	}

	/**
	 * Returns the search criteria
	 * 
	 * @return criteria
	 */
	public SearchCriteria getSearchCriteria() {
		return criteria;
	}

	/**
	 * Increment the total found with the number of results found in this batch
	 * 
	 * @param batchFound
	 *            number of results found in this batch
	 */
	protected void incrementFound(int batchFound) {
		totalFound += batchFound;
	}

	/**
	 * Returns the total number of results found (all batches)
	 * 
	 * @return totalFound
	 */
	public int getTotalFound() {
		return totalFound;
	}

	/**
	 * This method returns the theoretical results available if the value is
	 * more than the totalFound (indicating the actual amount that the search
	 * engine has found. This value is never 0 unless none are found.
	 * 
	 * @return totalFound or theoreticalAvailableResults
	 */
	public int calculateTheoreticalAvailableResults() {
		if (theoreticalAvailableResults < totalFound) {
			return totalFound;
		} else {
			return theoreticalAvailableResults;
		}
	}

	/**
	 * This method returns the theoretical results available without adjusting
	 * the value from 0. This means that this could be 0 or the total found (for
	 * adapters that do not support FOUND).
	 * 
	 * @return theoreticalAvailableResults
	 */
	public int getTheoreticalAvailableResults() {
		return this.theoreticalAvailableResults;
	}

	/**
	 * Sets the theoretical total number of results (maximum possible but not
	 * obtained)
	 * 
	 * @param theoreticalAvailableResults
	 */
	public void setTheoreticalAvailableResults(int theoreticalAvailableResults) {
		this.theoreticalAvailableResults = theoreticalAvailableResults;
	}

	/**
	 * Sets the diagnostic code as set by adapters that use it
	 * 
	 * @param diagnosticCode
	 * 
	 */
	public void setDiagnosticCode(String diagnosticCode) {
		this.diagnosticCode = diagnosticCode;
		DeusExMachina._SearchResultService().setDiagnostic(getSearchCriteria().getToken(), this.properties.getAdapterCode(), diagnosticCode);
	}

	/**
	 * Return the diagnostic code as set by adapters that use it
	 * 
	 * @return diagnosticCode
	 * 
	 */
	public String getDiagnosticCode() {
		return diagnosticCode;
	}

	/**
	 * 
	 * @return
	 */
	public Map getFacetQueryCounts() {
		return facetQueryCounts;
	}

	/**
	 * Set the facetQueryCounts
	 */
	public void setFacetQueryCounts(Map facetCounts) {
		this.facetQueryCounts = facetCounts;
	}

	/**
	 * fireStateMachine - Is called from a SearchTask and is the state machine
	 * for all the adapters
	 * 
	 * The logic can be understood by viewing FlowChart 5 in the Distributed
	 * Search Manager Design Document
	 * 
	 */
	public synchronized void fireStateMachine() {
		// Flog.info(toString(), "1 ADAPTER "+adapterInstanceTag+"Priming...");
		// State machine
		if (!superEgo && action_Prime()) {
			// Flog.info(toString(), "2 ADAPTER "+adapterInstanceTag+"Primed.
			// Performing...");
			// Query Ready
			if (!superEgo && action_Perform()) {
				// Flog.info(toString(), "3 ADAPTER
				// "+adapterInstanceTag+"Performed. Evaluating Next Batch...");
				// Search Performed once
				// Now keep getting until target lesser of maxResults / found
				// reached
				while (!superEgo && action_NextBatch()) {
					// More batch(es) need to be gotten
					if (action_Prime()) {
						// New batch query ready
						if (!action_Perform()) {
							// Something went wrong getting next batch
							setStatus(SMEvents.SEARCH_ADAPTER_STATUS_FAILED);
							break;
						}
					} else {
						// Something went wrong building the next batch query
						setStatus(SMEvents.SEARCH_ADAPTER_STATUS_FAILED);
						break;
					}
				}

				// Flog.info(toString(), "4 ADAPTER
				// "+adapterInstanceTag+"Cleanup...");
				// Cleanup adapter
				if (!action_StopNice()) {
					// Force a final cleanup
					action_StopForce();
				}

				if (getStatus() != SMEvents.SEARCH_ADAPTER_STATUS_FAILED) {
					// SUCCESS! Got valid results from this adapter
					setStatus(SMEvents.SEARCH_ADAPTER_STATUS_SUCCESSFUL);
				}
			} else {
				// Could not execute this search
				setStatus(SMEvents.SEARCH_ADAPTER_STATUS_FAILED);
			}
		} else {
			// Could not build the initial query.
			setStatus(SMEvents.SEARCH_ADAPTER_STATUS_FAILED);
		}
		finished = true;

		if (this.getResponseCode() < 100) {
			this.setResponseCode(getStatus());
		}

	} // fireStateMachine

	/**
	 * Terminate this adapter's operations
	 * 
	 */
	public void terminate() {
		superEgo = true;

		if (this.getStatus() < 100 && totalFound <= 0) // its busy doing stuff
														// with no results yet
		{
			this.setResponseCode(SMEvents.SEARCH_ADAPTER_STATUS_TIMEOUT);
		}
	}

	/**
	 * action_Prime is the wrapper for the doPrime directive that is implemented
	 * in an extended adapter.
	 * 
	 * @return boolean true if priming was successful
	 * 
	 */
	protected boolean action_Prime() {
		boolean stat = false;
		setStatus(SMEvents.SEARCH_ADAPTER_STATUS_BUILDING_QUERY);
		try {
			clearURLBuffer();
			doPrime();
			setStatus(SMEvents.SEARCH_ADAPTER_STATUS_BUILDING_QUERY_OK);
			stat = true;
		} catch (Throwable ee) {
			Flog.error(toString(), "SEARCH_ADAPTER_STATUS_BUILDING_QUERY", ee);
			stat = false;
		}
		return stat;
	} // action_Prime

	/**
	 * action_Perform is the wrapper for the doPerform directive that is
	 * implemented in an extended adapter.
	 * 
	 * @return boolean true if performance was successful
	 * 
	 */
	protected boolean action_Perform() {
		boolean stat = false;
		setStatus(SMEvents.SEARCH_ADAPTER_STATUS_GETTING_RESULTS);
		try {
			ArrayList list = doPerform();
			if (null != list) {
				if (list.size() > 0) {
					if (getProperties().getReRank()) {
						list = action_Rerank(list);
					}
					incrementFound(list.size());
					DeusExMachina._SearchResultService().addResults(getSearchCriteria().getToken(), list, getProperties().getAdapterCode(), calculateTheoreticalAvailableResults(), getFacetQueryCounts());
				}
				setStatus(SMEvents.SEARCH_ADAPTER_STATUS_GETTING_RESULTS_OK);
				stat = true;
				/*
				 * for (int i=0;i<batch.size();i++) { SearchResult res =
				 * (SearchResult)batch.get(i); System.err.println(" Title:
				 * "+res.getTitle()+" ["+res.getSource()+"]");
				 * System.err.println(" URL: "+res.getLink());
				 * System.err.println(" Description: "+res.getDescription());
				 * System.err.println("================================================================================="); }
				 */
			}
		} catch (Throwable ee) {
			Flog.error(toString(), "SEARCH_ADAPTER_STATUS_GETTING_RESULTS", ee);
			stat = false;
		}
		return stat;
	} // action_Perform

	/**
	 * Called from within the action_Perform method, to rerank the batch results
	 * (list)
	 * 
	 * @param list
	 *            the result ArrayList
	 */
	private ArrayList action_Rerank(ArrayList list) {
		float highestHitFrequency = 0f;
		float hitFrequency = 0f;
		for (int i = 0; i < list.size(); i++) {
			hitFrequency = ((SearchResult) list.get(i)).getHitFrequency();
			if (hitFrequency > highestHitFrequency) {
				highestHitFrequency = hitFrequency;
			}
		}
		if (highestHitFrequency <= 0f) {
			highestHitFrequency = 0.1f; // we have to make this as close as
										// possible to zero as it will cause
										// DivideByZero RuntimeException o/w.
		}

		if (highestHitFrequency > 0f) {
			for (int i = 0; i < list.size(); i++) {
				SearchResult result = (SearchResult) list.get(i);
				result.setRelevance(ResultSetUnifier.staticRelevance(result.getRelevance(), result.getHitFrequency(), highestHitFrequency));
			}
		}
		return list;
	}

	/**
	 * action_NextBatch is the wrapper for the doNextBatch directive that is
	 * implemented in an extended adapter.
	 * 
	 * @return boolean true if there is a next batch to get
	 * 
	 */
	protected boolean action_NextBatch() {
		boolean stat = false;
		setStatus(SMEvents.SEARCH_ADAPTER_STATUS_NEXT_BATCH_LOGIC);
		try {
			stat = doNextBatch();
		} catch (Throwable ee) {
			setStatus(SMEvents.SEARCH_ADAPTER_STATUS_FAILED);
			Flog.error(toString(), "SEARCH_ADAPTER_STATUS_NEXT_BATCH_LOGIC", ee);
			stat = false;
		}
		return stat;
	} // action_NextBatch

	/**
	 * action_StopNice is the wrapper for the doStopNice directive that is
	 * implemented in an extended adapter. If not nicely stopped, force stop is
	 * called.
	 * 
	 * @return boolean true if the adapter has cleaned up properly
	 * 
	 */
	protected boolean action_StopNice() {
		boolean stat = false;
		setStatus(SMEvents.SEARCH_ADAPTER_STATUS_NICE_STOPPING);
		try {
			doStopNice();
			stat = true;
		} catch (Throwable ee) {
			Flog.error(toString(), "SEARCH_ADAPTER_STATUS_NICE_STOPPING: Reason: " + ee.getMessage(), null);
			stat = false;
		}
		return stat;
	} // action_StopNice

	/**
	 * action_StopForce is the wrapper for the doStopForce directive that is
	 * implemented in an extended adapter.
	 * 
	 */
	protected void action_StopForce() {
		setStatus(SMEvents.SEARCH_ADAPTER_STATUS_FORCE_STOPPING);
		try {
			doStopForce();
		} catch (Throwable ee) {
			Flog.error(toString(), "SEARCH_ADAPTER_STATUS_FORCE_STOPPING: Reason: " + ee.getMessage(), null);
		}
		return;
	} // action_StopForce

	/** ABSTRACT METHODS * */

	/**
	 * doPrime() abstract method is used to construct the request url based on
	 * the search criteria. This is where the DSM search parameters are mapped
	 * to the remote search parameters. Hence priming the adapter allows the
	 * adapter to initiate the appropriate search. Success is reported to
	 * action_Prime via no thrown Exceptions. This does NOT mean that you catch
	 * everything in fear of failure. In many cases, it is advisable to allow
	 * failure to prevent useless searches. Any errors are caught on the
	 * action_Prime level and will not interrupt the other adapters.
	 * 
	 * 
	 * @throws Throwable
	 * @exception Throwable
	 */
	public abstract void doPrime() throws Throwable;

	/**
	 * doPerform() abstract method is where the results are obtained. Any logic
	 * for updating the total number of search results or theoretical total
	 * should be handled here by the implementing classes. It also calls the
	 * rerank action if it is specified in the adapter's configuration. Success
	 * is reported to action_Prime via no thrown Exceptions. This does NOT mean
	 * that you catch everything in fear of failure. In many cases, it is
	 * advisable to allow failure to prevent useless searches. Any errors are
	 * caught on the action_Prime level and will not interrupt the other
	 * adapters.
	 * 
	 * @throws Throwable
	 * @return ArrayList an Arraylist (not null) containing SearchResult
	 *         objects.
	 * @exception Throwable
	 */
	public abstract ArrayList doPerform() throws Throwable;

	/**
	 * doNextBatch() abstract method allows the adapter to perform the next
	 * batch logic. This entails perhaps figuring out what the next page number
	 * or result index to look for should be. Success is reported to
	 * action_Prime via no thrown Exceptions. This does NOT mean that you catch
	 * everything in fear of failure. In many cases, it is advisable to allow
	 * failure to prevent useless searches. Any errors are caught on the
	 * action_Prime level and will not interrupt the other adapters.
	 * 
	 * 
	 * @throws Throwable
	 * @exception Throwable
	 */
	public abstract boolean doNextBatch() throws Throwable;

	/**
	 * doStopNice() abstract method should be used to cleanup variables used in
	 * your adapter Maximum cleanup should be done here. Any failure will result
	 * in doStopForce.
	 * 
	 * @throws Throwable
	 * @exception Throwable
	 */
	public abstract void doStopNice() throws Throwable;

	/**
	 * doStopForce() abstract method should be used to cleanup variables used in
	 * your adapter absolute minimum cleanup here for fast forceful stop.
	 * 
	 * @throws Throwable
	 * @exception Throwable
	 */
	public abstract void doStopForce() // be careful with this one, it can
										// create looped exceptions
			throws Throwable;

	/**
	 * toString() method as a representation of an extended class, used for
	 * logging and other flagging
	 * 
	 * @return String the adapter's full path
	 * 
	 */
	public String toString() {
		return "o.m.d.a.SearchAdapter" + "_" + adapterInstanceTag;
	}
}

// -- EOF
